from playwright.sync_api import Page, Locator, ElementHandle, TimeoutError
import time
import random
from typing import List, Union, Optional, Callable
import logging

class PlaywrightUtils:
    """
    Playwright 工具类，提供常用的页面操作方法，包含超时重试机制
    """
    
    def __init__(self, page: Page, logger=None):
        """
        初始化工具类
        
        Args:
            page: Playwright 页面对象
            logger: 日志记录器，如果为 None 则使用默认日志记录器
        """
        self.page = page
        self.logger = logger or logging.getLogger("playwright_utils")
    
    def wait_for_navigation(self, timeout: int = 30000, url: str = None, wait_until: str = "load"):
        """
        等待页面导航完成
        
        Args:
            timeout: 超时时间（毫秒）
            url: 期望导航到的 URL（可选）
            wait_until: 等待条件，可选值: 'load', 'domcontentloaded', 'networkidle'
        
        Returns:
            bool: 导航是否成功
        """
        try:
            self.page.wait_for_load_state(wait_until, timeout=timeout)
            if url and not self.page.url.startswith(url):
                self.logger.warning(f"页面导航完成，但URL不匹配。期望: {url}, 实际: {self.page.url}")
                return False
            return True
        except Exception as e:
            self.logger.error(f"等待页面导航超时: {e}")
            return False
    
    def find_element(self, selector: str, timeout: int = 5000, retries: int = 2) -> Optional[ElementHandle]:
        """
        查找元素，支持超时重试
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 每次尝试的超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            ElementHandle: 元素句柄，如果未找到则返回 None
        """
        for attempt in range(retries + 1):
            try:
                return self.page.query_selector(selector, timeout=timeout)
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"查找元素 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"查找元素 {selector} 失败: {e}")
                    return None
    
    def find_elements(self, selector: str, timeout: int = 5000, retries: int = 2) -> List[ElementHandle]:
        """
        查找多个元素，支持超时重试
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 每次尝试的超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            List[ElementHandle]: 元素句柄列表，如果未找到则返回空列表
        """
        for attempt in range(retries + 1):
            try:
                # 先等待至少一个元素出现
                self.page.wait_for_selector(selector, timeout=timeout)
                return self.page.query_selector_all(selector)
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"查找元素集合 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"查找元素集合 {selector} 失败: {e}")
                    return []
    
    def get_locator(self, selector: str) -> Locator:
        """
        获取 Locator 对象
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
        
        Returns:
            Locator: Playwright Locator 对象
        """
        return self.page.locator(selector)
    
    def get_locator_all(self, selector: str) -> List[Locator]:
        """
        获取所有匹配的 Locator 对象
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
        
        Returns:
            List[Locator]: Locator 对象列表
        """
        return self.page.locator(selector).all()
    
    def is_element_exists(self, selector: str, timeout: int = 1000) -> bool:
        """
        检查元素是否存在
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
        
        Returns:
            bool: 元素存在返回 True，否则返回 False
        """
        try:
            return self.page.query_selector(selector, timeout=timeout) is not None
        except Exception:
            return False
    
    def is_element_visible(self, selector: str, timeout: int = 1000) -> bool:
        """
        检查元素是否可见
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
        
        Returns:
            bool: 元素可见返回 True，否则返回 False
        """
        try:
            element = self.page.query_selector(selector, timeout=timeout)
            return element is not None and element.is_visible()
        except Exception:
            return False

    @staticmethod
    def wait_for_element(page: Page, selector: str, timeout: int = 3000, state: str = "visible", retries: int = 2) -> Optional[ElementHandle]:
        """
        等待元素出现
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
            state: 等待状态，可选值: 'attached', 'detached', 'visible', 'hidden'
            retries: 重试次数
        
        Returns:
            ElementHandle: 元素句柄，如果未找到则返回 None
        """
        for attempt in range(retries + 1):
            try:
                return page.wait_for_selector(selector, state=state, timeout=timeout)
            except Exception as e:
                if attempt < retries:
                    time.sleep(1)
                else:
                    return None
    
    def wait_for_element_to_be_stable(self, selector: str, timeout: int = 10000, check_interval: int = 500) -> Optional[ElementHandle]:
        """
        等待元素稳定（位置不再变化）
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
            check_interval: 检查间隔（毫秒）
        
        Returns:
            ElementHandle: 元素句柄，如果未找到或未稳定则返回 None
        """
        start_time = time.time()
        end_time = start_time + (timeout / 1000)
        
        last_rect = None
        stable_count = 0
        
        while time.time() < end_time:
            element = self.find_element(selector)
            if not element:
                time.sleep(check_interval / 1000)
                continue
            
            current_rect = element.bounding_box()
            if not current_rect:
                time.sleep(check_interval / 1000)
                continue
            
            if last_rect:
                # 检查位置是否变化
                position_changed = (
                    abs(current_rect['x'] - last_rect['x']) > 1 or
                    abs(current_rect['y'] - last_rect['y']) > 1 or
                    abs(current_rect['width'] - last_rect['width']) > 1 or
                    abs(current_rect['height'] - last_rect['height']) > 1
                )
                
                if not position_changed:
                    stable_count += 1
                    if stable_count >= 3:  # 连续3次位置稳定
                        return element
                else:
                    stable_count = 0
            
            last_rect = current_rect
            time.sleep(check_interval / 1000)
        
        self.logger.warning(f"等待元素 {selector} 稳定超时")
        return None
    
    def click(self, selector: str, timeout: int = 5000, retries: int = 2, force: bool = False) -> bool:
        """
        点击元素，支持超时重试
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
            retries: 重试次数
            force: 是否强制点击
        
        Returns:
            bool: 点击成功返回 True，否则返回 False
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return False
                
                # 确保元素可见且可点击
                if not element.is_visible():
                    element.scroll_into_view_if_needed()
                    time.sleep(0.5)
                
                element.click(force=force, timeout=timeout)
                return True
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"点击元素 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"点击元素 {selector} 失败: {e}")
                    return False
    
    def humanized_click(self, selector: str, timeout: int = 5000, retries: int = 2) -> bool:
        """
        模拟人类点击元素，添加随机延迟和移动
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            bool: 点击成功返回 True，否则返回 False
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return False
                
                # 确保元素可见且可点击
                if not element.is_visible():
                    element.scroll_into_view_if_needed()
                    time.sleep(random.uniform(0.3, 0.7))
                
                # 获取元素的边界框
                box = element.bounding_box()
                if not box:
                    if attempt < retries:
                        continue
                    return False
                
                # 计算元素中心点
                center_x = box['x'] + box['width'] / 2
                center_y = box['y'] + box['height'] / 2
                
                # 添加随机偏移
                offset_x = random.uniform(-box['width'] / 4, box['width'] / 4)
                offset_y = random.uniform(-box['height'] / 4, box['height'] / 4)
                
                # 移动到元素附近
                self.page.mouse.move(center_x + offset_x, center_y + offset_y)
                time.sleep(random.uniform(0.1, 0.3))
                
                # 移动到元素中心点
                self.page.mouse.move(center_x, center_y)
                time.sleep(random.uniform(0.05, 0.15))
                
                # 点击元素
                self.page.mouse.click(center_x, center_y)
                time.sleep(random.uniform(0.1, 0.2))
                
                return True
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"人性化点击元素 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"人性化点击元素 {selector} 失败: {e}")
                    return False
    
    def fill_text(self, selector: str, text: str, timeout: int = 5000, retries: int = 2, clear: bool = True) -> bool:
        """
        填充文本，支持超时重试
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            text: 要填充的文本
            timeout: 超时时间（毫秒）
            retries: 重试次数
            clear: 是否先清空文本
        
        Returns:
            bool: 填充成功返回 True，否则返回 False
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return False
                
                # 确保元素可见
                if not element.is_visible():
                    element.scroll_into_view_if_needed()
                    time.sleep(0.5)
                
                # 先聚焦元素
                element.focus()
                
                if clear:
                    # 清空文本
                    element.fill("")
                    time.sleep(0.2)
                
                # 填充文本
                element.fill(text)
                return True
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"填充文本到元素 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"填充文本到元素 {selector} 失败: {e}")
                    return False
    
    def humanized_type(self, selector: str, text: str, timeout: int = 5000, retries: int = 2, clear: bool = True) -> bool:
        """
        模拟人类输入文本，添加随机延迟
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            text: 要输入的文本
            timeout: 超时时间（毫秒）
            retries: 重试次数
            clear: 是否先清空文本
        
        Returns:
            bool: 输入成功返回 True，否则返回 False
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return False
                
                # 确保元素可见
                if not element.is_visible():
                    element.scroll_into_view_if_needed()
                    time.sleep(random.uniform(0.3, 0.7))
                
                # 先聚焦元素
                element.focus()
                time.sleep(random.uniform(0.1, 0.3))
                
                if clear:
                    # 清空文本
                    element.fill("")
                    time.sleep(random.uniform(0.2, 0.5))
                
                # 逐字输入文本
                for char in text:
                    self.page.keyboard.type(char)
                    time.sleep(random.uniform(0.05, 0.15))
                
                return True
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"人性化输入文本到元素 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"人性化输入文本到元素 {selector} 失败: {e}")
                    return False
    
    def get_text(self, selector: str, timeout: int = 5000, retries: int = 2) -> Optional[str]:
        """
        获取元素文本，支持超时重试
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            str: 元素文本，如果未找到则返回 None
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return None
                
                return element.inner_text().strip()
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"获取元素 {selector} 文本失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"获取元素 {selector} 文本失败: {e}")
                    return None
    
    def get_attribute(self, selector: str, attribute: str, timeout: int = 5000, retries: int = 2) -> Optional[str]:
        """
        获取元素属性，支持超时重试
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            attribute: 属性名
            timeout: 超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            str: 属性值，如果未找到则返回 None
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return None
                
                return element.get_attribute(attribute)
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"获取元素 {selector} 属性 {attribute} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"获取元素 {selector} 属性 {attribute} 失败: {e}")
                    return None
    
    def is_checkbox_checked(self, selector: str, timeout: int = 5000, retries: int = 2) -> Optional[bool]:
        """
        检查复选框是否被选中
        
        Args:
            selector: 复选框选择器
            timeout: 超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            bool: 复选框被选中返回 True，否则返回 False，如果未找到则返回 None
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return None
                
                # 通过类名检测
                class_attr = element.get_attribute("class") or ""
                if "checked" in class_attr or "selected" in class_attr:
                    return True
                
                # 通过原生属性检测
                checkbox_input = element.query_selector("input[type=checkbox]")
                if checkbox_input and checkbox_input.is_checked():
                    return True
                
                return False
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"检查复选框 {selector} 状态失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"检查复选框 {selector} 状态失败: {e}")
                    return None
    
    def wait_for_condition(self, condition: Callable[[], bool], timeout: int = 30000, check_interval: int = 500, error_message: str = None) -> bool:
        """
        等待条件满足
        
        Args:
            condition: 条件函数，返回 True 表示条件满足
            timeout: 超时时间（毫秒）
            check_interval: 检查间隔（毫秒）
            error_message: 超时时的错误消息
        
        Returns:
            bool: 条件满足返回 True，超时返回 False
        """
        start_time = time.time()
        end_time = start_time + (timeout / 1000)
        
        while time.time() < end_time:
            try:
                if condition():
                    return True
            except Exception as e:
                self.logger.debug(f"条件检查异常: {e}")
            
            time.sleep(check_interval / 1000)
        
        if error_message:
            self.logger.warning(error_message)
        else:
            self.logger.warning(f"等待条件满足超时 ({timeout}ms)")
        
        return False
    
    def scroll_to_element(self, selector: str, timeout: int = 5000, retries: int = 2) -> bool:
        """
        滚动到元素位置
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            timeout: 超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            bool: 滚动成功返回 True，否则返回 False
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return False
                
                element.scroll_into_view_if_needed()
                return True
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"滚动到元素 {selector} 失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"滚动到元素 {selector} 失败: {e}")
                    return False
    
    def scroll_page(self, direction: str = "down", distance: int = 300) -> None:
        """
        滚动页面
        
        Args:
            direction: 滚动方向，可选值: 'up', 'down', 'left', 'right'
            distance: 滚动距离（像素）
        """
        if direction == "up":
            self.page.evaluate(f"window.scrollBy(0, -{distance})")
        elif direction == "down":
            self.page.evaluate(f"window.scrollBy(0, {distance})")
        elif direction == "left":
            self.page.evaluate(f"window.scrollBy(-{distance}, 0)")
        elif direction == "right":
            self.page.evaluate(f"window.scrollBy({distance}, 0)")
    
    def wait_and_check_text(self, selector: str, expected_text: str, timeout: int = 5000, retries: int = 2) -> bool:
        """
        等待元素出现并检查其文本内容是否符合预期
        
        Args:
            selector: 元素选择器（XPath或CSS选择器）
            expected_text: 预期的文本内容
            timeout: 超时时间（毫秒）
            retries: 重试次数
        
        Returns:
            bool: 文本匹配返回 True，否则返回 False
        """
        for attempt in range(retries + 1):
            try:
                element = self.wait_for_element(selector, timeout=timeout)
                if not element:
                    if attempt < retries:
                        continue
                    return False
                
                actual_text = element.inner_text().strip()
                return expected_text in actual_text
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"检查元素 {selector} 文本失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(1)
                else:
                    self.logger.warning(f"检查元素 {selector} 文本失败: {e}")
                    return False
    
    def execute_with_retry(self, action: Callable, retries: int = 2, delay: float = 1.0, error_message: str = None) -> Optional[any]:
        """
        执行操作并自动重试
        
        Args:
            action: 要执行的操作函数
            retries: 重试次数
            delay: 重试延迟（秒）
            error_message: 失败时的错误消息
        
        Returns:
            any: 操作结果，如果失败则返回 None
        """
        for attempt in range(retries + 1):
            try:
                return action()
            except Exception as e:
                if attempt < retries:
                    self.logger.debug(f"操作执行失败，正在重试 ({attempt+1}/{retries}): {e}")
                    time.sleep(delay)
                else:
                    if error_message:
                        self.logger.warning(f"{error_message}: {e}")
                    else:
                        self.logger.warning(f"操作执行失败: {e}")
                    return None
                
    